package cn.yiya.shiji.views;

import android.content.Context;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.SystemClock;
import android.support.v4.text.TextUtilsCompat;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;

import org.xmlpull.v1.XmlPullParser;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import cn.yiya.shiji.R;
import cn.yiya.shiji.utils.ThemeUtil;
import cn.yiya.shiji.utils.ViewUtil;

/**
 * Created by chenjian on 2016/6/12.
 */
public class LineMorphingDrawable extends Drawable implements Animatable {

    private boolean mRunning = false;

    private Paint mPaint;

    private int mPaddingLeft = 12;
    private int mPaddingTop = 12;
    private int mPaddingRight = 12;
    private int mPaddingBottom = 12;

    private RectF mDrawBound;

    private int mPrevState;
    private int mCurState;
    private long mStartTime;
    private float mAnimProgress;
    private int mAnimDuration;
    private Interpolator mInterpolator;
    private int mStrokeSize;
    private int mStrokeColor;
    private boolean mClockwise;
    private Paint.Cap mStrokeCap;
    private Paint.Join mStrokeJoin;
    private boolean mIsRtl;

    private Path mPath;

    private State[] mStates;

    private LineMorphingDrawable(State[] states, int curState, int paddingLeft, int paddingTop, int paddingRight, int paddingBottom, int animDuration, Interpolator interpolator, int strokeSize, int strokeColor, Paint.Cap strokeCap, Paint.Join strokeJoin, boolean clockwise, boolean isRtl) {
        mStates = states;
        mPaddingLeft = paddingLeft;
        mPaddingTop = paddingTop;
        mPaddingRight = paddingRight;
        mPaddingBottom = paddingBottom;

        mAnimDuration = animDuration;
        mInterpolator = interpolator;
        mStrokeSize = strokeSize;
        mStrokeColor = strokeColor;
        mStrokeCap = strokeCap;
        mStrokeJoin = strokeJoin;
        mClockwise = clockwise;
        mIsRtl = isRtl;

        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeCap(mStrokeCap);
        mPaint.setStrokeJoin(mStrokeJoin);
        mPaint.setColor(mStrokeColor);
        mPaint.setStrokeWidth(mStrokeSize);

        mDrawBound = new RectF();

        mPath = new Path();

        switchLineState(curState, false);
    }

    @Override
    public void draw(Canvas canvas) {
        int restoreCount = canvas.save();
        float degrees = (mClockwise ? 180 : -180) * ((mPrevState < mCurState ? 0f : 1f) + mAnimProgress);

        if (mIsRtl)
            canvas.scale(-1f, 1f, mDrawBound.centerX(), mDrawBound.centerY());

        canvas.rotate(degrees, mDrawBound.centerX(), mDrawBound.centerY());
        canvas.drawPath(mPath, mPaint);
        canvas.restoreToCount(restoreCount);
    }

    @Override
    public void setAlpha(int alpha) {
        mPaint.setAlpha(alpha);
    }

    @Override
    public void setColorFilter(ColorFilter cf) {
        mPaint.setColorFilter(cf);
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }

    @Override
    protected void onBoundsChange(Rect bounds) {
        super.onBoundsChange(bounds);

        mDrawBound.left = bounds.left + mPaddingLeft;
        mDrawBound.top = bounds.top + mPaddingTop;
        mDrawBound.right = bounds.right - mPaddingRight;
        mDrawBound.bottom = bounds.bottom - mPaddingBottom;

        updatePath();
    }

    public void switchLineState(int state, boolean animation) {
        if (mCurState != state) {
            mPrevState = mCurState;
            mCurState = state;
            if (animation)
                start();
            else {
                mAnimProgress = 1f;
                updatePath();
            }
        } else if (!animation) {
            mAnimProgress = 1f;
            updatePath();
        }
    }

    public boolean setLineState(int state, float progress) {
        if (mCurState != state) {
            mPrevState = mCurState;
            mCurState = state;
            mAnimProgress = progress;
            updatePath();
            return true;
        } else if (mAnimProgress != progress) {
            mAnimProgress = progress;
            updatePath();
            return true;
        }

        return false;
    }

    public int getLineState() {
        return mCurState;
    }

    public int getLineStateCount() {
        return mStates == null ? 0 : mStates.length;
    }

    public float getAnimProgress() {
        return mAnimProgress;
    }

    private void updatePath() {
        mPath.reset();

        if (mStates == null)
            return;

        if (mAnimProgress == 0f || (mStates[mPrevState].links != null && mAnimProgress < 0.05f))
            updatePathWithState(mPath, mStates[mPrevState]);
        else if (mAnimProgress == 1f || (mStates[mCurState].links != null && mAnimProgress > 0.95f))
            updatePathWithState(mPath, mStates[mCurState]);
        else
            updatePathBetweenStates(mPath, mStates[mPrevState], mStates[mCurState], mInterpolator.getInterpolation(mAnimProgress));

        invalidateSelf();
    }

    private void updatePathWithState(Path path, State state) {
        if (state.links != null) {
            for (int i = 0; i < state.links.length; i += 2) {
                int index1 = state.links[i] * 4;
                int index2 = state.links[i + 1] * 4;

                float x1 = getX(state.points[index1]);
                float y1 = getY(state.points[index1 + 1]);
                float x2 = getX(state.points[index1 + 2]);
                float y2 = getY(state.points[index1 + 3]);

                float x3 = getX(state.points[index2]);
                float y3 = getY(state.points[index2 + 1]);
                float x4 = getX(state.points[index2 + 2]);
                float y4 = getY(state.points[index2 + 3]);

                if (x1 == x3 && y1 == y3) {
                    path.moveTo(x2, y2);
                    path.lineTo(x1, y1);
                    path.lineTo(x4, y4);
                } else if (x1 == x4 && y1 == y4) {
                    path.moveTo(x2, y2);
                    path.lineTo(x1, y1);
                    path.lineTo(x3, y3);
                } else if (x2 == x3 && y2 == y3) {
                    path.moveTo(x1, y1);
                    path.lineTo(x2, y2);
                    path.lineTo(x4, y4);
                } else {
                    path.moveTo(x1, y1);
                    path.lineTo(x2, y2);
                    path.lineTo(x3, y3);
                }
            }

            for (int i = 0, count = state.points.length / 4; i < count; i++) {
                boolean exist = false;
                for (int j = 0; j < state.links.length; j++)
                    if (state.links[j] == i) {
                        exist = true;
                        break;
                    }

                if (exist)
                    continue;

                int index = i * 4;

                path.moveTo(getX(state.points[index]), getY(state.points[index + 1]));
                path.lineTo(getX(state.points[index + 2]), getY(state.points[index + 3]));
            }
        } else {
            for (int i = 0, count = state.points.length / 4; i < count; i++) {
                int index = i * 4;

                path.moveTo(getX(state.points[index]), getY(state.points[index + 1]));
                path.lineTo(getX(state.points[index + 2]), getY(state.points[index + 3]));
            }
        }
    }

    private void updatePathBetweenStates(Path path, State prev, State cur, float progress) {
        int count = Math.max(prev.points.length, cur.points.length) / 4;

        for (int i = 0; i < count; i++) {
            int index = i * 4;

            float x1;
            float y1;
            float x2;
            float y2;
            if (index >= prev.points.length) {
                x1 = 0.5f;
                y1 = 0.5f;
                x2 = 0.5f;
                y2 = 0.5f;
            } else {
                x1 = prev.points[index];
                y1 = prev.points[index + 1];
                x2 = prev.points[index + 2];
                y2 = prev.points[index + 3];
            }

            float x3;
            float y3;
            float x4;
            float y4;
            if (index >= cur.points.length) {
                x3 = 0.5f;
                y3 = 0.5f;
                x4 = 0.5f;
                y4 = 0.5f;
            } else {
                x3 = cur.points[index];
                y3 = cur.points[index + 1];
                x4 = cur.points[index + 2];
                y4 = cur.points[index + 3];
            }

            mPath.moveTo(getX(x1 + (x3 - x1) * progress), getY(y1 + (y3 - y1) * progress));
            mPath.lineTo(getX(x2 + (x4 - x2) * progress), getY(y2 + (y4 - y2) * progress));
        }
    }

    private float getX(float value) {
        return mDrawBound.left + mDrawBound.width() * value;
    }

    private float getY(float value) {
        return mDrawBound.top + mDrawBound.height() * value;
    }

    //Animation: based on http://cyrilmottier.com/2012/11/27/actionbar-on-the-move/

    private void resetAnimation() {
        mStartTime = SystemClock.uptimeMillis();
        mAnimProgress = 0f;
    }

    public void cancel() {
        stop();
        setLineState(mCurState, 1f);
    }

    @Override
    public void start() {
        resetAnimation();

        scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
        invalidateSelf();
    }

    @Override
    public void stop() {
        if (!isRunning())
            return;

        mRunning = false;
        unscheduleSelf(mUpdater);
        invalidateSelf();
    }

    @Override
    public boolean isRunning() {
        return mRunning;
    }

    @Override
    public void scheduleSelf(Runnable what, long when) {
        mRunning = true;
        super.scheduleSelf(what, when);
    }

    private final Runnable mUpdater = new Runnable() {

        @Override
        public void run() {
            update();
        }

    };

    private void update() {
        long curTime = SystemClock.uptimeMillis();
        float value = Math.min(1f, (float) (curTime - mStartTime) / mAnimDuration);

        if (value == 1f) {
            setLineState(mCurState, 1f);
            mRunning = false;
        } else
            setLineState(mCurState, mInterpolator.getInterpolation(value));

        if (isRunning())
            scheduleSelf(mUpdater, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION);
    }

    public static class State {
        float[] points;
        int[] links;

        public State() {
        }

        public State(float[] points, int[] links) {
            this.points = points;
            this.links = links;
        }
    }

    public static class Builder {
        private int mCurState;

        private int mPaddingLeft;
        private int mPaddingTop;
        private int mPaddingRight;
        private int mPaddingBottom;

        private int mAnimDuration;
        private Interpolator mInterpolator;
        private int mStrokeSize;
        private int mStrokeColor;
        private boolean mClockwise;
        private Paint.Cap mStrokeCap;
        private Paint.Join mStrokeJoin;
        private boolean mIsRtl;

        private State[] mStates;

        private static final String TAG_STATE_LIST = "state-list";
        private static final String TAG_STATE = "state";
        private static final String TAG_POINTS = "points";
        private static final String TAG_LINKS = "links";
        private static final String TAG_ITEM = "item";

        public Builder() {
        }

        public Builder(Context context, int defStyleRes) {
            this(context, null, 0, defStyleRes);
        }

        public Builder(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LineMorphingDrawable, defStyleAttr, defStyleRes);
            int resId;

            if ((resId = a.getResourceId(R.styleable.LineMorphingDrawable_lmd_state, 0)) != 0)
                states(readStates(context, resId));
            curState(a.getInteger(R.styleable.LineMorphingDrawable_lmd_curState, 0));
            padding(a.getDimensionPixelSize(R.styleable.LineMorphingDrawable_lmd_padding, 0));
            paddingLeft(a.getDimensionPixelSize(R.styleable.LineMorphingDrawable_lmd_paddingLeft, mPaddingLeft));
            paddingTop(a.getDimensionPixelSize(R.styleable.LineMorphingDrawable_lmd_paddingTop, mPaddingTop));
            paddingRight(a.getDimensionPixelSize(R.styleable.LineMorphingDrawable_lmd_paddingRight, mPaddingRight));
            paddingBottom(a.getDimensionPixelSize(R.styleable.LineMorphingDrawable_lmd_paddingBottom, mPaddingBottom));
            animDuration(a.getInteger(R.styleable.LineMorphingDrawable_lmd_animDuration, context.getResources().getInteger(android.R.integer.config_mediumAnimTime)));
            if ((resId = a.getResourceId(R.styleable.LineMorphingDrawable_lmd_interpolator, 0)) != 0)
                interpolator(AnimationUtils.loadInterpolator(context, resId));
            strokeSize(a.getDimensionPixelSize(R.styleable.LineMorphingDrawable_lmd_strokeSize, ThemeUtil.dpToPx(context, 2)));
            strokeColor(a.getColor(R.styleable.LineMorphingDrawable_lmd_strokeColor, 0xFFFFFFFF));
            int cap = a.getInteger(R.styleable.LineMorphingDrawable_lmd_strokeCap, 0);
            if (cap == 0)
                strokeCap(Paint.Cap.BUTT);
            else if (cap == 1)
                strokeCap(Paint.Cap.ROUND);
            else
                strokeCap(Paint.Cap.SQUARE);
            int join = a.getInteger(R.styleable.LineMorphingDrawable_lmd_strokeJoin, 0);
            if (join == 0)
                strokeJoin(Paint.Join.MITER);
            else if (join == 1)
                strokeJoin(Paint.Join.ROUND);
            else
                strokeJoin(Paint.Join.BEVEL);
            clockwise(a.getBoolean(R.styleable.LineMorphingDrawable_lmd_clockwise, true));

            int direction = a.getInteger(R.styleable.LineMorphingDrawable_lmd_layoutDirection, View.LAYOUT_DIRECTION_LTR);
            if (direction == View.LAYOUT_DIRECTION_LOCALE)
                rtl(TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_RTL);
            else
                rtl(direction == View.LAYOUT_DIRECTION_RTL);

            a.recycle();
        }

        private State[] readStates(Context context, int id) {
            XmlResourceParser parser = null;
            List<State> states = new ArrayList<>();

            try {
                parser = context.getResources().getXml(id);

                int eventType = parser.getEventType();
                String tagName;
                boolean lookingForEndOfUnknownTag = false;
                String unknownTagName = null;

                // This loop will skip to the state-list start tag
                do {
                    if (eventType == XmlPullParser.START_TAG) {
                        tagName = parser.getName();
                        if (tagName.equals(TAG_STATE_LIST)) {
                            eventType = parser.next();
                            break;
                        }
                        throw new RuntimeException("Expecting menu, got " + tagName);
                    }
                    eventType = parser.next();
                } while (eventType != XmlPullParser.END_DOCUMENT);

                boolean reachedEndOfStateList = false;
                State state = null;
                List<String> array = new ArrayList<>();
                StringBuilder currentValue = new StringBuilder();

                while (!reachedEndOfStateList) {
                    switch (eventType) {
                        case XmlPullParser.START_TAG:
                            if (lookingForEndOfUnknownTag)
                                break;

                            tagName = parser.getName();
                            switch (tagName) {
                                case TAG_STATE:
                                    state = new State();
                                    break;
                                case TAG_POINTS:
                                case TAG_LINKS:
                                    array.clear();
                                    break;
                                case TAG_ITEM:
                                    currentValue.delete(0, currentValue.length());
                                    break;
                                default:
                                    lookingForEndOfUnknownTag = true;
                                    unknownTagName = tagName;
                                    break;
                            }
                            break;

                        case XmlPullParser.END_TAG:
                            tagName = parser.getName();

                            if (lookingForEndOfUnknownTag && tagName.equals(unknownTagName)) {
                                lookingForEndOfUnknownTag = false;
                                unknownTagName = null;
                            }

                            switch (tagName) {
                                case TAG_STATE_LIST:
                                    reachedEndOfStateList = true;
                                    break;
                                case TAG_STATE:
                                    states.add(state);
                                    break;
                                case TAG_POINTS:
                                    state.points = new float[array.size()];
                                    for (int i = 0; i < state.points.length; i++)
                                        state.points[i] = Float.parseFloat(array.get(i));
                                    break;
                                case TAG_LINKS:
                                    state.links = new int[array.size()];
                                    for (int i = 0; i < state.links.length; i++)
                                        state.links[i] = Integer.parseInt(array.get(i));
                                    break;
                                case TAG_ITEM:
                                    array.add(currentValue.toString());
                                    break;
                            }

                            break;

                        case XmlPullParser.TEXT:
                            currentValue.append(parser.getText());
                            break;

                        case XmlPullParser.END_DOCUMENT:
                            reachedEndOfStateList = true;
                            break;
                    }

                    eventType = parser.next();
                }

            } catch (Exception e) {
            } finally {
                if (parser != null)
                    parser.close();
            }

            if (states.isEmpty())
                return null;

            return states.toArray(new State[states.size()]);
        }

        public LineMorphingDrawable build() {
            if (mStrokeCap == null)
                mStrokeCap = Paint.Cap.BUTT;

            if (mStrokeJoin == null)
                mStrokeJoin = Paint.Join.MITER;

            if (mInterpolator == null)
                mInterpolator = new AccelerateInterpolator();

            return new LineMorphingDrawable(mStates, mCurState, mPaddingLeft, mPaddingTop, mPaddingRight, mPaddingBottom, mAnimDuration, mInterpolator, mStrokeSize, mStrokeColor, mStrokeCap, mStrokeJoin, mClockwise, mIsRtl);
        }

        public Builder states(State... states) {
            mStates = states;
            return this;
        }

        public Builder curState(int state) {
            mCurState = state;
            return this;
        }

        public Builder padding(int padding) {
            mPaddingLeft = padding;
            mPaddingTop = padding;
            mPaddingRight = padding;
            mPaddingBottom = padding;
            return this;
        }

        public Builder paddingLeft(int padding) {
            mPaddingLeft = padding;
            return this;
        }

        public Builder paddingTop(int padding) {
            mPaddingTop = padding;
            return this;
        }

        public Builder paddingRight(int padding) {
            mPaddingRight = padding;
            return this;
        }

        public Builder paddingBottom(int padding) {
            mPaddingBottom = padding;
            return this;
        }

        public Builder animDuration(int duration) {
            mAnimDuration = duration;
            return this;
        }

        public Builder interpolator(Interpolator interpolator) {
            mInterpolator = interpolator;
            return this;
        }

        public Builder strokeSize(int size) {
            mStrokeSize = size;
            return this;
        }

        public Builder strokeColor(int strokeColor) {
            mStrokeColor = strokeColor;
            return this;
        }

        public Builder strokeCap(Paint.Cap cap) {
            mStrokeCap = cap;
            return this;
        }

        public Builder strokeJoin(Paint.Join join) {
            mStrokeJoin = join;
            return this;
        }

        public Builder clockwise(boolean clockwise) {
            mClockwise = clockwise;
            return this;
        }

        public Builder rtl(boolean rtl) {
            mIsRtl = rtl;
            return this;
        }

    }
}
